原型
每个 JavaScript 中的对象都有一个特殊的内部隐藏属性 [[prototype]]
,它要么指向 null
,要么指向另一个对象,我们称之为原型。当我们从一个对象上读取某个属性,如果在对象本身上没有找到,那么 JavaScript 引擎会尝试从它的原型中去寻找。
原型属性 [[prototype]]
虽然是内部隐藏的属性,但是有一些方法可以获取到它。
__proto__
其中之一是 __proto__
,但是 __proto__
并不完全等同于原型,只是由于历史原因,各个浏览器包括 NodeJS 都部署了这一属性,实际开发中应该使用 ES 规范中的更现代的设置原型的方法,后面会提到。考虑如下代码:
1 | // 代码片段 1 |
rabbit
对象本身没有 eats
属性,但是因为让它的原型指向了 animal
对象,这时我们可以说 animal
是 rabbit
的原型。所以引擎会在 rabbit
没有某个属性时从其原型 animal
上获取。
因此,我们可以将许多有用,但是更加通用的属性放到抽象程度更高的对象中,显然,在这里是 animal
:
1 | // 代码片段 2 |
现在 rabbit
通过原型,继承了 animal
的共同属性 eats
和 walk
。原型链 prototype chain 还可以更长:
1 | // 代码片段 3 |
现在,按照抽象程度由低到高形成了一条原型链:longEar
-> rabbit
-> animal
。
原型链的限制
对于原型链来说主要由下面 3 条限制:
- 原型链不能构成环,否则引擎将会抛出错误,在上面的例子中,即不能再将
animal
的原型指向longEar
。 __proto__
的值类型要么是null
,要么是Object
,即另一个对象。所有基本类型的值会被忽略。- 一个对象只能有一个原型,即不能继承两个父级。
this
的值
当对象调用原型的方法时,this
的值是指代原型还是对象本身呢?下面的代码清晰的说明了这一问题:
1 | const user = { |
不管某个方法是在对象本身上,还是在其原型上,this
永远指向 .
运算符前面的对象。
F.prototype
我们知道,对象除了可以使用字面量创建,还可以使用 new F()
形式的构造函数来创建。请注意,构造函数本质上依然是函数,JavaScript 中的每个函数都具有一个特殊的 prototype
属性。默认情况下,它是一个仅包含 constructor
属性的对象(不包括每个对象的隐藏属性 [[prototype]]
,因为函数也是对象),其中 constructor
指向函数本身:
1 | const F = function() {}; |
如果我们让 F.prototype
指向一个对象,那么在使用 new
操作符调用构造函数 F
时,新创建的实例的原型 [[prototype]]
都将被指向这个对象。示例:
1 | const animal = { |
上面的代码中,另一个需要注意的问题是 F.prototype.constructor
的值。改变构造函数 F.prototype
的默认指向,如果接下来(之前创建的实例不会受到影响)使用到其新创建的实例的 construtor
属性,会导致意想不到的错误。拿上面的例子来说,如果没有 (*)
这一行代码,rabbit.constructor
的值将会指向 animal
,显然这在人意料之外,也不合理。所以,普遍的共识是:不要依赖 constructor
这一属性,因为它能被任意修改。
有时候,我们需要使用某个实例的构造函数来创建一个实例,但是却不知道它的构造函数。这时可以这样做,但是需要小心:
1 | const User = function(name) { |
上面的代码可以如正常运行,因为构造函数 User
的 prototype
的指向是默认的,指向其自身。假如修改一下代码,改变其指向:
1 | const User = function(name) { |
我们来看看 (*)
这一行发生了什么:
- 首先引擎会在
user
对象上查找constructor
属性,没有找到。 - 接着引擎会沿着原型链查找,
user
的原型是User.prototype
,它是一个空对象{}
,没有找到。 - 空对象
{}
的原型是Object.prototype
,而Object.prototype.constructor === Object
。所以实际执行的是new Object('Sunny')
,而内置的对象构造函数会忽略所有参数,所以得到如上结果。
Object.prototype
让我们来看下面的代码:
1 | const obj = {}; |
为什么会有以上的输出?我们定义的对象是空的,是哪里来的代码生成了 [object Object]
这样的字符串信息?答案是内置的 toString
方法。通过字面量创建对象等同于使用对象构造函数 new Object()
。而 Object
构造函数就像所有函数一样,也有一个 prototype
属性,它指向了一个很大的对象,上面部署了 constructor
, toString
, valueOf
等一系列所有对象通用的方法和属性。
以下代码可以检查上面所说:
1 | const obj = {}; |
另外,Object.prototype
这个内置的原型的 [[prototype]]
指向了谁呢?答案是 null
:
1 | alert(Object.prototype.__proto__); // null |
其他抽象程度稍低的一些内置对象,比如 Function
,Array
,Date
等,也部署了一些方法在其原型上。这样每一个函数实例,数组实例或者日期实例,都可以使用一些内置于其原型上的方法了。这样设计的目的非常利于节省内存。下面这张图局部地说明了这种关系。

我们可以手动地验证一下:
1 | const arr = [1, 2, 3]; |
基本类型的原型
对于 String
, Number
, Boolean
这 3 种基本类型而言,当访问它们的属性时,会使用内置的构造函数(比如 Number()
)创建一个临时的封装对象,然后访问这个临时封装对象原型上的属性或者方法,访问完成后临时对象就消失了,这一过程对我们来说都是不可见的。
对于 null
和 undefined
而言,不会有自动创建临时封装对象这一过程,因为它们没有可用的属性或者方法,也就没有内置的原型。
设置原型的现代方法
我们应该只考虑以下 3 种现代的设置原型的方法:
Object.create(proto [, descriptors]) - 以给定的
proto
作为原型,创建一个新的空对象,descriptors
是可选参数,代表属性描述符。Object.getPrototypeOf(obj) - 返回对象的原型。
Object.setPrototypeOf(obj, proto) - 将
obj
的原型设为proto
。
让我们用更标准、现代的方法改写前面的例子:
1 | const animal = { |
使用原型方法进行浅拷贝
1 | const origin = {...}; |
上面的代码,以源对象的原型为原型,加上源对象的所有属性描述符选项,复制了所有源对象的属性,不管是可枚举属性还是不可枚举属性,数据属性还是访问属性,也包括了 Symbol
属性;同时有着相同的原型。
让我们看一个详细的例子:
1 | // 源对象 |
上面代码的源对象是一个出于演示目的而定制的,包含了可枚举属性和不可枚举属性,数据属性和访问属性,以及 Symbol
属性,从最后的打印结果可以看到,拷贝对象全部成功地复制了过来。
不要轻易在初始化更改原型设置
以上面的代码为例,如果我们在一开始让 rabbit
以 animal
为原型,接下来不要轻易更改 rabbit
的原型,因为引擎会对已有的原型继承做很多属性读取之类的优化,更改原型会破坏这些已有的优化,导致运行速度变慢。
纯字典对象
假设我们想要实现一个纯字典对象,即包含一切合法字符串为键的键值对,使用通常的字面量对象会有一个问题:有一个特殊的字符串 __proto__
无法如预期般奏效。
1 | const dictionary = { |
上面的属性读取无法得到预期的 some value
结果,因为通常字面量创建的对象,默认原型是 Object.prototype
,而因为历史原因 __proto__
这一特殊属性被当做访问原型的途径。如果将其赋值为基本类型值,赋值操作将会被忽略。
如何解决上面的问题?有两种方式:
- 使用
Map
,通常这是更加推荐的做法。 - 使用
Object.create(null)
从所有原型的顶端null
继承,这样创建的对象是一个真正的纯对象,不会包含一般对象内置的toString
,valueOf
,__proto__
等属性。
属性遍历方法比较
Object.keys(obj) / Object.values(obj) / Object.entries(obj)
– 返回一个包含对象自身的可枚举字符串属性的键 / 值 / 键值对的数组。不包含继承属性,不包含Symbol
属性。Object.getOwnPropertySymbols(obj)
– 返回一个包含自身所有Symbol
属性的数组。Object.getOwnPropertyNames(obj)
– 返回一个包含自身所有字符串属性的数组。Reflect.ownKeys(obj)
– 返回一个包含自身所有属性的数组。obj.hasOwnProperty(key)
- 如果自身(非继承)包含该属性,返回真。for...in
循环 - 遍历所有可枚举的字符串属性,包括自身属性和继承属性。不包括不可枚举属性,不包括Symbol
属性。
基于原型实现继承
虽然 ES6 引入了 class
关键字,但那只是一种语法糖。JavaScript 中继承的实现本质上是基于原型的。原型继承的实现参考以下代码:
1 | function Animal(name) { |